package svc.elib.db;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;

import svc.elib.util.FastRAFReader;

/**
 * eLib database, normalized according to the name and sex lookups
 * @author svc
 * @date 5.6.2013
 */
public class Database {
	
	private String databaseName;
	
	// list of papers in database
	private LinkedList<Paper> papers;
	
	/**
	 * Author Name --> Author map
	 */
	private HashMap<String, Author> authorIndex = 
		new HashMap<String, Author>();
	
	/**
	 * List of authors
	 */
	private LinkedList<Author> authors = 
		new LinkedList<Author>();
	
	/**
	 * Language -> Paper map
	 */
	private HashMap<String, LinkedList<Paper>> langIndex =
		new HashMap<String, LinkedList<Paper>>();
	
	/**
	 * Journal -> Paper map
	 */
	private HashMap<String, LinkedList<Paper>> journalIndex = 
		new HashMap<String, LinkedList<Paper>>();
	
	/**
	 * Papers per year map
	 */
	private HashMap<Integer, LinkedList<Paper>> yearIndex = 
		new HashMap<Integer, LinkedList<Paper>>();
	
	private int firstYear;
	private int lastYear;
	
	private int numNameCorrections = 0;
	private int numSexCorrections = 0;
	private int numShortFullNameMatches = 0;
	
	private NameCorrector nameCorrector;
	private SexCorrector sexCorrector;
	
	private HashMap<String, HashSet<String>> shortNames = 
		new HashMap<String, HashSet<String>>();
	
	/**
	 * Forms the database from a list of papers
	 * 
	 * @param databaseName
	 * @param papers
	 */
	public Database(String databaseName, LinkedList<Paper> papers) {
		this.databaseName = databaseName;
		this.papers = new LinkedList<Paper>();
		
		firstYear = Integer.MAX_VALUE;
		lastYear = Integer.MIN_VALUE;
		
		for (Paper p : papers) {
			Paper newPaper = new Paper(p.getId(), p.getLang(), p.getJournal(), p.getYear());
			
			LinkedList<Author> authors = p.getAuthors();
			LinkedList<Author> newAuthors = new LinkedList<Author>();
			for (Author a : authors) {
				String aName = a.getName();
				Author newAuthor = authorIndex.get(aName);
				if (newAuthor == null) {
					newAuthor = new Author(a.getName(), a.isMale());
					authorIndex.put(aName, newAuthor);
				}
				newAuthor.addPaper(newPaper);
				newAuthors.add(newAuthor);
			}
			
			newPaper.setAuthors(newAuthors);
			updateIndex(langIndex, newPaper.getLang(), newPaper);
			updateIndex(journalIndex, newPaper.getJournal(), newPaper);
			updateIndex(yearIndex, newPaper.getYear(), newPaper);
			this.papers.add(newPaper);
			
			int y = newPaper.getYear();
			if (y < firstYear)
				firstYear = y;
			
			if (y > lastYear)
				lastYear = y;
		}
		
		Set<Entry<String, Author>> es = authorIndex.entrySet();
		Iterator<Entry<String, Author>> it = es.iterator();
		while (it.hasNext()) {
			Author a = it.next().getValue();
			authors.addLast(a);
		}
	}
	
	public Database(String dataSourceFile, int firstYear, int lastYear) 
		throws IOException
	{
		this.firstYear = firstYear;
		this.lastYear = lastYear;
		this.databaseName = dataSourceFile;
		this.papers = new LinkedList<Paper>();
		
		this.nameCorrector = new NameCorrector();
		this.sexCorrector = new SexCorrector();
		
		load(dataSourceFile);
	}
	
	/**
	 * Loads database from file
	 * @param dataSourceFile
	 */
	private void load(String dataSourceFile) 
		throws IOException
	{
		Properties prop = new Properties();
		prop.put("FileToRead", dataSourceFile);
		prop.put("BufferSize", String.valueOf(1024 * 16));
		prop.put("MaxLineLength", String.valueOf(1024 * 16));
		prop.put("MaxFields", "64");
		prop.put("TextEncoding", "UTF-8");
		prop.put("FieldSeparator", ";");
		
		FastRAFReader frf = new FastRAFReader(prop);
		while(-1 != frf.nextLine()) {
			String raw = frf.getLastLineAsSBU().toString();
			String normalized = normalize(raw);
			StringTokenizer st = new StringTokenizer(normalized, ";", false);
			
			String id = st.nextToken().trim();
			String lang = normalizeLanguage(st.nextToken().trim());
			String journal = st.nextToken().trim();
			int year = Integer.parseInt(st.nextToken().trim());
			
			// check the year scope of the publication
			if (year < firstYear || year > lastYear)
				continue;
			
			Paper p = new Paper(id, lang, journal, year);
			
			updateIndex(langIndex, lang, p);
			updateIndex(journalIndex, journal, p);
			updateIndex(yearIndex, p.getYear(), p);
			
			while (st.hasMoreTokens()) {
				String firstName = st.nextToken().trim();
				String lastName = st.nextToken().trim();
				String sex = st.nextToken().trim();
				boolean male = sex.compareToIgnoreCase("m") == 0 ? true : false;
				
				String originalName = lastName + "_" + firstName;
				String authorName = "";
				
				if (nameCorrector.hasToBeCorrected(firstName, lastName)) {
					authorName = nameCorrector.getCorrection(firstName, lastName);
					++numNameCorrections;
				} else {
					authorName = lastName + "_" + firstName;
				}
				
				if (sexCorrector.hasToBeCorrected(originalName)) {
					male = sexCorrector.isMale(originalName);
					++numSexCorrections;
				} 
				else
				if (sexCorrector.hasToBeCorrected(authorName)) {
					male = sexCorrector.isMale(authorName);
					++numSexCorrections;
				}
				
				Author author = authorIndex.get(authorName);
				if (author == null) {
					author = new Author(authorName, male);
					authorIndex.put(authorName, author);
					//authors.addLast(author);
				}
				
				author.addPaper(p);
				p.addAuthor(author);
				
				if (!author.isShortName()) {
					String shName = author.getShortName();
					HashSet<String> fullNames = shortNames.get(shName);
					if (fullNames == null) {
						fullNames = new HashSet<String>();
						fullNames.add(authorName);
						shortNames.put(shName, fullNames);
					} else {
						fullNames.add(authorName);
					}
				} 
			}
			
			papers.addLast(p);
		}
		
		joinShortFullNames();
		Set<Entry<String, Author>> es = authorIndex.entrySet();
		Iterator<Entry<String, Author>> it = es.iterator();
		while (it.hasNext()) {
			Author a = it.next().getValue();
			authors.addLast(a);
		}
		
		frf.close();
	}
	
	/**
	 * Determines full names that uniquely can be matched to short 
	 * names. However, there is condition when matched short and full
	 * names are unified (number of papers > 2 and activity span >= 5)
	 */
	private void joinShortFullNames() {
		Set<Entry<String, HashSet<String>>> es = shortNames.entrySet();
		Iterator<Entry<String, HashSet<String>>> it = es.iterator();
		while (it.hasNext()) {
			Entry<String, HashSet<String>> e = it.next();
			String shortName = e.getKey();
			HashSet<String> fullNames = e.getValue();
			
			Author shortNameAuthor = authorIndex.get(shortName);
			
			if (shortNameAuthor != null && fullNames.size() == 1) {
				String fullName = fullNames.iterator().next();
				Author fullNameAuthor = authorIndex.get(fullName);
				
				if (shortNameAuthor.isMale() == fullNameAuthor.isMale() 	
					&& shortNameAuthor.getNumPapers() > 2 && fullNameAuthor.getNumPapers() > 2
				    //&& shortNameAuthor.getFirstYear() < 1995 && 
				    && shortNameAuthor.getActivitySpan() >= 5 && fullNameAuthor.getActivitySpan() >= 5) 
				{
					// move papers from shortNameAuthor to fullNameAuthor
					// remove short authors from papers
					LinkedList<Paper> shPapers = shortNameAuthor.getPapers();
					for (int i = 0; i < shPapers.size(); i++) {
						Paper p = shPapers.get(i);
						fullNameAuthor.addPaper(p);
						p.mustBeRemovedAuthor(shortNameAuthor);
						p.addAuthor(fullNameAuthor);
					}
				
					// remove shortNameAuthor from authorIndex
					Author a = authorIndex.remove(shortName);
					if (authorIndex.containsKey(shortName) || authorIndex.containsKey(a.getName())) {
						System.out.println("Something wrong... ");
					}
					
					shortNameAuthor = null;
				
					++numShortFullNameMatches;
				}
			}
		}
	}
	
	private String normalizeLanguage(String lang) {
		if (lang.compareTo("Eng") == 0)
			return "eng";
		else
		if (lang.compareTo("deu") == 0)
			return "ger";
		else
		if (lang.compareTo("fra") == 0)
			return "fre";
		else
		if (lang.compareTo("srp") == 0 || lang.compareTo("срп") == 0)
			return "srb";
		
		return lang;
	}
	
	/*
	private void updateIndex(HashMap<String, LinkedList<Paper>> index, String key, Paper p) {
		LinkedList<Paper> papers = index.get(key);
		if (papers == null) {
			papers = new LinkedList<Paper>();
			papers.addLast(p);
			index.put(key, papers);
		} else {
			papers.addLast(p);
		}
	}
	*/
	
	private <T> void updateIndex(HashMap<T, LinkedList<Paper>> index, T key, Paper p) {
		LinkedList<Paper> papers = index.get(key);
		if (papers == null) {
			papers = new LinkedList<Paper>();
			papers.addLast(p);
			index.put(key, papers);
		} else {
			papers.addLast(p);
		}
	}
	
	private int numKeysInIndex(HashMap<String, LinkedList<Paper>> index) {
		return index.keySet().size();
	}
	
	private String getKeys(HashMap<String, LinkedList<Paper>> index) {
		Iterator<String> it = index.keySet().iterator();
		StringBuilder sb = new StringBuilder();
		while (it.hasNext()) {
			String k = it.next();
			sb.append(k).append("|");
		}
		return sb.toString();
	}
	
	private String normalize(String inputLine) {
		int end = inputLine.length() - 1;
		while (inputLine.charAt(end) == ';')
			end--;
		
		return inputLine.substring(0, end + 1).replace(";;", "; ;");
	}
	
	public int getFirstYear() {
		return firstYear;
	}
	
	public int getLastYear() {
		return lastYear;
	}
	
	public int getNumPapers() {
		return papers.size();
	}
	
	public LinkedList<Paper> getPapers() {
		return papers;
	}
	
	public String info() {
		StringBuilder sb = new StringBuilder();
		sb.append("eLib database\n");
		sb.append("#papers          = ").append(papers.size()).append("\n");
		sb.append("#authors         = ").append(authors.size()).append("\n");
		sb.append("firstYear        = ").append(firstYear).append("\n");
		sb.append("lastYear         = ").append(lastYear).append("\n");
		sb.append("#nameCorrections = ").append(numNameCorrections).append("\n");
		sb.append("#sexCorrections  = ").append(numSexCorrections).append("\n");
		sb.append("#short <--> full matches = ").append(numShortFullNameMatches).append("\n");
		sb.append("#journals        = ").append(numKeysInIndex(journalIndex)).append("\n");
		sb.append("journals         = ").append(getKeys(journalIndex)).append("\n");
		sb.append("#languages       = ").append(numKeysInIndex(langIndex)).append("\n");
		sb.append("languages        = ").append(getKeys(langIndex)).append("\n");
		
		int numShortNames = 0;
		int numMales = 0;
		int numFemales = 0;
		for (int i = 0; i < authors.size(); i++) {
			Author a = authors.get(i);
			if (a.isShortName())
				++numShortNames;
			if (a.isMale()) 
				++numMales;
			else
				++numFemales;
		}
		sb.append("#shortNames      = ").append(numShortNames).append("\n");
		sb.append("#males           = ").append(numMales).append("\n");
		sb.append("#females         = ").append(numFemales).append("\n");
		
		int oneAuthorPaper = 0;
		for (int i = 0; i < papers.size(); i++) {
			Paper p = papers.get(i);
			if (p.getAuthors().size() == 1) {
				oneAuthorPaper++;
			}
		}
		sb.append("#oneAuthorPaper = ").append(oneAuthorPaper).append("\n");
		
		return sb.toString();
	}
	
	public void dumpAuthors() {
		int maxPublications = Integer.MIN_VALUE;
		Author maxPub = null;
		int maxActivitySpan = Integer.MIN_VALUE;
		Author maxActivity = null;
		
		for (int i = 0; i < authors.size(); i++) {
			Author a = authors.get(i);
			//System.out.println(a.getInfo());
			
			if (a.getActivitySpan() > maxActivitySpan) {
				maxActivitySpan = a.getActivitySpan();
				maxActivity = a;
			}
			
			if (a.getNumPapers() > maxPublications) {
				maxPublications = a.getNumPapers();
				maxPub = a;
			}
		}
		
		System.out.println("Maximal number of publications: " + maxPub.getInfo());
		maxPub.printFullInfo();
		System.out.println("Maximal activity span         : " + maxActivity.getInfo());
		maxActivity.printFullInfo();
	}
	
	public Author searchAuthor(String firstName, String lastName) {
		String authorName = "";
		if (nameCorrector.hasToBeCorrected(firstName, lastName)) {
			authorName = nameCorrector.getCorrection(firstName, lastName);
			System.out.println("Search author, corrected name to " + authorName);
		} else {
			authorName = lastName + "_" + firstName;
		}
		
		return authorIndex.get(authorName);
	}
	
	public LinkedList<Author> getAuthors() {
		return authors;
	}
	
	public String getName() {
		return databaseName;
	}
	
	public HashMap<String, LinkedList<Paper>> getJournalIndex() {
		return journalIndex;
	}
	
	public HashMap<Integer, LinkedList<Paper>> getYearIndex() {
		return yearIndex;
	}
	
	// test
	public static void main(String[] args) 
		throws IOException
	{
		Database db = new Database("eLibData.csv", 1932, 2011);
		System.out.println(db.info());
		db.dumpAuthors();
	}
}
 